# -*- coding: utf-8 -*-
"""
    generate_case_csv_file.py
    ~~~~~~~~~~~~~~~~~~~~~~~

    将测试用例xmind转换为禅道导入的格式


    :author: JoTsai
    :copyright: (c) 2019, Tungee
    :date created: 2019-12-22
    :python version: 3.7

    安装依赖：
        pip install requests
        pip install xmindparser

    Usage:
        1. 将xmind文件导出为csv文件：
            python generate_case_csv_file.py xmind文件

        2. 将xmind文件导出为csv文件，并创建对应的禅道用例模块，同步到csv中：
            python generate_case_csv_file.py xmind文件 禅道项目名称 禅道账号 禅道密码

        3. 读取csv文件，创建对应的禅道用例模块，同步到csv中：
            python generate_case_csv_file.py csv文件 禅道项目名称 禅道账号 禅道密码

        4. 导出的csv文件需自行手动导入禅道，导入时注意检查。

"""
import sys
import os
import csv
import json
import hashlib
from pprint import pprint
import requests
import re
from xmindparser import xmind_to_dict


class ZentaoSession(requests.Session):
    url_root = 'http://zentao.tangees.com/zentao/index.php'
    url_login = url_root + '?m=user&f=login&t=json'  # 登录地址
    url_get_modules = url_root + '?m=testcase&t=json'  # 功能测试用例地址
    url_pre_page = url_root + '?m=tree&f=browse&productID={_id}&view=case'  # 维护模块地址
    url_create_modules = url_root + '?m=tree&f=manageChild&root={_id}&view=case&t=json'  # 维护模块--创建子模块

    def login(self, account, password):
        """
        登录禅道
        :param account: 账号
        :param password: 密码
        :return: 登录状态：成功True，失败False
        """
        cred = {
            'account': account,
            'password': hashlib.md5(password.encode('utf8')).hexdigest(),
            'keepLogin[]': 'on'
        }
        r = self.post(self.url_login, data=cred)
        return r.status_code == 200 and r.json()['status'] == 'success'

    def set_product_in_cookies(self, product_name):
        """
         http://zentao.tangees.com/zentao/index.php?m=testcase&f=browse&t=json
        :param product_name:
        :return:
        """
        r = self.get(self.url_get_modules)
        if r.status_code == 200 and r.json()['status'] == 'success':
            products = json.loads(r.json()['data'])['products']
            self.product_path_id_map = {v: k for k, v in products.items()}
        if product_name in self.product_path_id_map:
            self.product_id = self.product_path_id_map[product_name]
        else:
            print('项目不存在：' + product_name)
            exit(0)
        temp_cookie = requests.cookies.RequestsCookieJar()
        temp_cookie.set('lastProduct', str(self.product_id), domain='zentao.tangees.com', path='/zentao/')
        temp_cookie.set('preProductID', str(self.product_id), domain='zentao.tangees.com', path='/zentao/')
        self.cookies.update(temp_cookie)
        print(temp_cookie)
        return self

    def get_modules_of_product(self, product_name):
        """
        获取项目下的所有模块
        :param product_name:项目名称
        :return:模块，字典
        """
        self.set_product_in_cookies(product_name)
        r = self.get(self.url_get_modules)
        if r.status_code == 200 and r.json()['status'] == 'success':
            self.zentao_data = json.loads(r.json()['data'])
            modules = self.zentao_data['modules']
            if modules == ['/']:
                modules = {'0': '/'}
            self.module_path_id_map = {v: k for k, v in modules.items()}
            return self.module_path_id_map
        else:
            print('Failed to get modules of product')

    def get_new_modules(self, modules, product_name):
        self.get_modules_of_product(product_name)
        zentao_modules = list(self.module_path_id_map.keys())
        new_modules = [_ for _ in modules if _ not in zentao_modules]
        return new_modules

    def trans_modules_to_tree(self, modules):
        module_tree = {}
        for m in modules:
            m_path = m[1:].split('/')
            current_root = module_tree
            for p in m_path:
                if p not in current_root:
                    current_root[p] = {}
                current_root = current_root[p]
        return module_tree

    def create_module_tree(self, module_tree, product_name, pre_module_str=''):
        self.get_modules_of_product(product_name)
        next_layer_modules = list(module_tree.keys())
        must_create_modules = [
            _ for _ in next_layer_modules
            if pre_module_str + '/' + _ not in self.module_path_id_map
        ]
        if len(must_create_modules) > 0:
            if pre_module_str:
                parent_module_id = self.module_path_id_map[pre_module_str]
            else:
                parent_module_id = '0'
            self.create_modules_in_target(
                parent_module_id, must_create_modules
            )
        for k, v in module_tree.items():
            self.create_module_tree(
                v, product_name, pre_module_str=pre_module_str + '/' + k)

    def create_modules_in_target(self, parent_id, modules):
        for m in modules:
            url = self.url_create_modules.format(_id=self.product_id)
            print('url: ' + url)
            r = self.post(
                url, data={'modules[]': m, 'parentModuleID': parent_id}
            )
            r.encoding = 'utf-8'
            if 'alert' in r.text:
                print('在模块%s下创建模块%s时返回警告：%s' % (parent_id, m, str(r.text)))
            else:
                print('在模块%s下创建模块%s成功' % (parent_id, m))

    def create_case_modules(self, modules, product_name):
        """
        在禅道中生成对应模块
        :param modules:
        :param product_name:
        :return:
        """
        new_modules = self.get_new_modules(modules, product_name)
        print('需要创建的禅道新用例模块：')
        print(new_modules)
        module_tree = self.trans_modules_to_tree(new_modules)
        self.create_module_tree(module_tree, product_name)

    def update_csv_case_module_id(self, csv_file, product_name):
        """
        本地csv文件添加对应的禅道模块id
        :param csv_file:
        :param product_name:
        :return:
        """
        with open(csv_file, 'r', encoding='gbk') as f:
            reader = csv.reader(f)
            case_list = list(reader)
        self.get_modules_of_product(product_name)
        for case in case_list[1:]:
            if case[0] in self.module_path_id_map:
                case[0] = '%s(#%s)' % (case[0], self.module_path_id_map[case[0]])
            else:
                print('模块不存在或已包含id：' + case[0])
        with open(csv_file, 'w', newline='', encoding='gbk') as csvfile:
            writer = csv.writer(csvfile)
            writer.writerows(case_list)
            print('已更新csv文件中的所属模块，添加了模块id')


def parser_tree_to_list(root_value, tree, current_layer, pre_topics, case_list, keep_layer=None):
    """
    :param root_value:根主题值
    :param tree: xmind树
    :param current_layer:
    :param pre_topics: 除尾部层级外，xmind树主题值，list类型
    :param case_list:
    :param keep_layer: 待保留的层级数
    :return:主题值、主题所在的层级
    """
    if 'topics' not in tree:  # 如果当前主题没有子主题
        pre_topics.pop()
        found_max_layer = current_layer
        return tree['title'], found_max_layer  # 返回当前主题值和主题层级
    else:
        found_max_layer = 0
        tail_topic_list = []  # 末尾层级主题值，list类型
        for node in tree['topics']:  # node，节点
            pre_topics.append(node['title'])
            tail_topic, max_layer = parser_tree_to_list(root_value=root_value, tree=node, current_layer=current_layer + 1, pre_topics=pre_topics, case_list=case_list, keep_layer=keep_layer)
            found_max_layer = max(found_max_layer, max_layer)
            tail_topic_list.append(tail_topic)
        layer_to_final = found_max_layer - current_layer  # 最大层级-当前层级，即离末尾层级差值
        if layer_to_final == 2:
            # print(pre_topics, "00000000000000000000")  # ['测试使用模块', '话术库', 'Q入口检查', '成功路径', '移动端点击【快捷话术】，打开快捷话术页面', '3', '我是前置条件']
            # print(found_max_layer, "111111111111111")  # 9
            # print(current_layer, "22222222222222222")  # 7
            # print(layer_to_final, "3333333333333333")  # 2
            # print(tail_topic_list, "444444444444444")  # [['点击【快捷话术】', '打开快捷话术页面']]
            # 生成步骤和预期
            step_list = []
            expect_list = []
            for i, step_and_expect in enumerate(tail_topic_list):  # i step_and_expect -----> 0 ['点击【快捷话术】', '打开快捷话术页面']
                if isinstance(step_and_expect, str):
                    step_list.append('%d. %s' % (i + 1, step_and_expect))
                    expect_list.append('%d. ' % (i + 1))
                else:
                    step_list.append('%d. %s' % (i + 1, step_and_expect[0]))
                    expect_list.append('%d. %s' % (i + 1, step_and_expect[1]))
            title_split_index = -1  # 标题分割线下标，即优先级的下标,默认没有前置条件，即为-1
            # 生成所属模块,即成功路径、失败路径前面的主题部分
            for mark in ['成功路径', '失败路径']:
                if mark in pre_topics[:title_split_index]:
                    if keep_layer:
                        split_index = keep_layer + 1
                    else:
                        split_index = pre_topics.index(mark)  # 获取'成功路径', '失败路径'的下标
                    case_module = '/' + '/'.join(pre_topics[:split_index])
                    break
            else:
                case_module = '/' + '/'.join(pre_topics[:title_split_index - 1])
            # 生成关键词
            # if pre_topics[-2] is None:
            #     keyword = root_value
            # else:
            #     keyword = root_value + ';\n' + pre_topics[-2]
            # 生成用例标题、优先级和前置条件
            for mark in ['成功路径', '失败路径']:
                if mark in pre_topics[:title_split_index]:
                    split_index = pre_topics.index(mark)  # 获取'成功路径\失败路径'的下标
                    if len(pre_topics[split_index:-1]) == 3:  # 根据'成功路径\失败路径'后面的节点数量判定是否包含前置条件
                        title_case = '-'.join(pre_topics[:split_index - 1]) + ":" + '-'.join(pre_topics[split_index - 1:split_index + 2])  # 生成用例标题
                        priority = pre_topics[title_split_index - 1]
                        preconditions = pre_topics[title_split_index]
                        break
            else:
                title_case = '-'.join(pre_topics[:title_split_index - 3]) + ":" + '-'.join(pre_topics[title_split_index - 3:title_split_index])  # 生成用例标题
                priority = pre_topics[title_split_index]
                preconditions = None
            # 生成用例步骤
            step_case = '\n'.join(step_list)
            # 生成用例预期
            expect_case = '\n'.join(expect_list)
            # 添加用例
            # ["所属模块", "相关需求", "优先级", "关键词", "前置条件", "用例标题", "步骤", "预期", "用例类型", "适用阶段"]
            # ['/探迹营销/话术库/Q入口检查', None, '3', None, None, '探迹营销-话术库-Q入口检查-成功路径-移动端点击【快捷话术】，打开快捷话术页面', '1. 点击【快捷话术】', '1. 打开快捷话术页面', '功能测试', '功能测试阶段']
            case_list.append([case_module, None, priority, None, preconditions, title_case, step_case, expect_case, "功能测试", "功能测试阶段"])
            pre_topics.pop()
            return '', found_max_layer
        elif layer_to_final < 2:
            if len(tail_topic_list) > 1:
                print('错误：步骤有多于1个的预期！！！')
                # print(str(pre_topics) + ', ' + tree['title'])
                print(f'步骤路径为：{"-".join(pre_topics)}')
                exit(0)
            else:
                pre_topics.pop()  # 删除列表pre_topics末尾值
                return [tree['title'], tail_topic_list[0]], found_max_layer  # 返回最后一层的步骤和预期
        else:
            pre_topics.pop()
            return '', found_max_layer


def tree_to_case_list(tree, root_value):
    """
    将xmind树转换为每一行的用例结构
    :param tree:  xmind树,即每一个画布sheet中的页面内容
    :param root_value: 当前画布的根主题值
    :return: 所有用例，list类型  [['/探迹营销/话术库/Q入口检查', None, '3', None, None, '探迹营销-话术库-Q入口检查-成功路径-移动端点击【快捷话术】，打开快捷话术页面', '1. 点击【快捷话术】', '1. 打开快捷话术页面', '功能测试', '功能测试阶段'],.....]
    """

    _case_list = []
    n = input("请输入保留的模块层级数(不能小于3)：")
    if not re.findall('^[0-9]+$', n):
        keep_layer = None  # None时，截取”成功路径或失败路径“前面的部分作为模块路径
    else:
        keep_layer = int(n) - 2
        if keep_layer <= 0:
            print("输入的层级数不能小于3")
            exit()
    for topic in tree['topic']['topics']:  # 遍历第二级主题xmind树
        parser_tree_to_list(root_value=root_value, tree=topic, current_layer=1, pre_topics=[topic['title']], case_list=_case_list, keep_layer=keep_layer)
    return _case_list


def get_modules_from_xmind(xmind_file):
    xmind_tabs = xmind_to_dict(xmind_file)
    xmind_tree = xmind_tabs[0]
    root_topic_value = xmind_tree['topic']['title']
    case_list = tree_to_case_list(xmind_tree, root_topic_value)
    _modules = list(set([_[0] for _ in case_list]))
    return _modules


def get_modules_from_csv(csv_file):
    """
    获取用例模块
    :param csv_file: csv文件地址
    :return: 用例模块
    """
    with open(csv_file, 'r', encoding='gbk') as csvfile:
        reader = csv.reader(csvfile)
        case_list = list(reader)
    _modules = list(set([_[0] for _ in case_list[1:]]))
    return _modules


def generate_case(xmind_file, save_path):
    """
    生成测试用例
    :param xmind_file: xmind文件路径
    :param save_path: csv保存路径
    :return:None
    """
    xmind_tabs = xmind_to_dict(xmind_file)  # 获取xmind文件中所有内容，列表类型，每个元素为一个画布sheet内容
    xmind_tree = xmind_tabs[0]  # 获取第一个画布sheet内容,dict类型
    root_topic_value = xmind_tree['topic']['title']  # 获取根主题的值
    # 按用例组织excel的行
    case_list = tree_to_case_list(xmind_tree, root_topic_value)
    # 生成csv格式的测试用例
    # [['/探迹营销/话术库/Q入口检查', None, '3', None, None, '探迹营销-话术库-Q入口检查-成功路径-移动端点击【快捷话术】，打开快捷话术页面', '1. 点击【快捷话术】', '1. 打开快捷话术页面', '功能测试', '功能测试阶段'],.....]
    csv_case_list = [["所属模块", "相关需求", "优先级", "关键词", "前置条件", "用例标题", "步骤", "预期", "用例类型", "适用阶段"]]
    csv_case_list.extend(case_list)
    # 将用例写入csv文件
    with open(save_path, 'w', newline='', encoding='utf-8') as csvfile:
        writer = csv.writer(csvfile)
        writer.writerows(csv_case_list)
        print('用例xmind已成功导出为csv文件，不包含模块id')


if __name__ == '__main__':
    xmind_file = sys.argv[1]  # python generate_case_csv_file.py xmind文件 禅道项目名称 禅道账号 禅道密码
    target_path = os.path.splitext(xmind_file)[0] + '.csv'
    if xmind_file[-6:] == '.xmind' and os.path.exists(xmind_file):
        generate_case(xmind_file, save_path=target_path)  # 生成测试用例
    elif xmind_file[-4:] == '.csv' and os.path.exists(xmind_file):
        target_path = xmind_file
        print('源文件为csv，将更新所属模块id……')
    else:
        print('用例xmind文件不存在：' + xmind_file)
        exit(0)
    if len(sys.argv) > 2:
        if len(sys.argv) < 5:
            print('同步到禅道需要额外参数：禅道项目名称 禅道账号 禅道密码')
            exit(0)
        else:
            # python generate_case_csv_file.py csv文件 禅道项目名称 禅道账号 禅道密码
            target_product_name = sys.argv[2]  # 禅道项目名称
            zentao_account = sys.argv[3]  # 禅道账号
            zentao_password = sys.argv[4]  # 禅道密码
            case_modules = get_modules_from_csv(target_path)  # 获取csv文件中的模块
            zentao_user = ZentaoSession()
            zentao_user.login(zentao_account, zentao_password)  # 登录禅道
            zentao_user.create_case_modules(case_modules, target_product_name)
            # 更新用例csv
            zentao_user.update_csv_case_module_id(target_path, target_product_name)
    else:
        print('只有1个参数，不进行任何禅道操作')
